package com.tong.cloud.feign.client;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONException;
import feign.Client;
import feign.Request;
import feign.Response;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.util.StopWatch;
import org.springframework.util.StreamUtils;

import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLSocketFactory;
import java.io.ByteArrayInputStream;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Collection;
import java.util.Map;

import static feign.Util.ensureClosed;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.apache.commons.io.IOUtils.toByteArray;

@Slf4j
public class LogClient extends Client.Default {
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    public LogClient(SSLSocketFactory socketFactory, HostnameVerifier hostnameVerifier) {
        super(socketFactory, hostnameVerifier);
    }

    @Override
    public Response execute(Request request, Request.Options options) throws IOException {
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();

        Exception exception = null;
        BufferingFeignClientResponse bufferingFeignClientResponse = null;
        try {
            bufferingFeignClientResponse = new BufferingFeignClientResponse(super.execute(request, options));
        } catch (Exception exp) {
            log.error(exp.getMessage(), exp);
            exception = exp;
            throw exp;
        } finally {
            stopWatch.stop();
            this.logAndSave(request, bufferingFeignClientResponse, stopWatch, exception);
        }

        Response response = bufferingFeignClientResponse.getResponse().toBuilder()
                .body(bufferingFeignClientResponse.getBody(), bufferingFeignClientResponse.getResponse().body().length())
                .build();
        bufferingFeignClientResponse.close();

        return response;
    }

    private void logAndSave(Request request, BufferingFeignClientResponse bufferingFeignClientResponse, StopWatch stopWatch, Exception exception) {
        // 组装request及response信息
        StringBuilder sb = new StringBuilder("[log started]\r\n");
        sb.append(request.httpMethod()).append(" ").append(request.url()).append("\r\n");
        // 请求Header
        combineHeaders(sb, request.headers());
        combineRequestBody(sb, request.body(), request.requestTemplate().queries());
        sb.append("\r\nResponse cost time(ms)： ").append(stopWatch.getLastTaskTimeMillis());
        if (bufferingFeignClientResponse != null) {
            sb.append("  status: ").append(bufferingFeignClientResponse.status());
        }
        sb.append("\r\n");
        if (bufferingFeignClientResponse != null) {
            // 响应Header
            combineHeaders(sb, bufferingFeignClientResponse.headers());
            combineResponseBody(sb, bufferingFeignClientResponse.toBodyString(), bufferingFeignClientResponse.headers().get(HttpHeaders.CONTENT_TYPE));
        }
        if (exception != null) {
            sb.append("Exception:\r\n  ").append(exception.getMessage()).append("\r\n");
        }
        sb.append("\r\n[log ended]");
        log.debug(sb.toString());
        // 保存日志信息至缓存,可替换成MySQL或者MongoDB存储
        redisTemplate.opsForValue().set("sbLog" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss")), sb.toString());
    }

    private static void combineHeaders(StringBuilder sb, Map<String, Collection<String>> headers) {
        if (headers != null && !headers.isEmpty()) {
            sb.append("Headers:\r\n");
            for (Map.Entry<String, Collection<String>> ob : headers.entrySet()) {
                for (String val : ob.getValue()) {
                    sb.append("  ").append(ob.getKey()).append(": ").append(val).append("\r\n");
                }
            }
        }
    }

    private static void combineRequestBody(StringBuilder sb, byte[] body, Map<String, Collection<String>> params) {
        if (params != null) {
            sb.append("Request Params:\r\n").append("  ").append(params).append("\r\n");
        }
        if (body != null && body.length > 0) {
            sb.append("Request Body:\r\n").append("  ").append(new String(body)).append("\r\n");
        }
    }

    private static void combineResponseBody(StringBuilder sb, String respStr, Collection<String> collection) {
        if (respStr == null) {
            return;
        }
        if (collection.contains(MediaType.APPLICATION_JSON_VALUE)) {
            try {
                respStr = JSON.parseObject(respStr).toString();
                //no care this exception
            } catch (JSONException ignored) {
            }
        }
        sb.append("Body:\r\n").append(respStr).append("\r\n");
    }

    static final class BufferingFeignClientResponse implements Closeable {
        private final Response response;
        private byte[] body;

        private BufferingFeignClientResponse(Response response) {
            this.response = response;
        }

        private Response getResponse() {
            return this.response;
        }

        private int status() {
            return this.response.status();
        }

        private Map<String, Collection<String>> headers() {
            return this.response.headers();
        }

        private String toBodyString() {
            try {
                return new String(toByteArray(getBody()), UTF_8);
            } catch (Exception e) {
                return super.toString();
            }
        }

        private InputStream getBody() throws IOException {
            if (this.body == null) {
                this.body = StreamUtils.copyToByteArray(this.response.body().asInputStream());
            }
            return new ByteArrayInputStream(this.body);
        }

        @Override
        public void close() {
            ensureClosed(response);
        }
    }

}
